package com.github.m6d21.conflict.util;

import static com.github.m6d21.conflict.util.FileUtil.exists;

import com.github.m6d21.conflict.scanner.ClassConflictScanner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *<p>
 *
 *</p>
 *
 * @author liuyang
 * @date 2018/12/11 Tue 17:17:00
 * @since 1.0.0
 */
public class ConflictScanHelper {

    private static final Logger LOG = LoggerFactory.getLogger(ConflictScanHelper.class);

    private static final String JAR_SUFFIX = ".jar";
    private static final String CLASS_SUFFIX = ".class";
    private static final String CLASSPATH_DIR_SUFFIX = "classes";

    private static final String SPLIT_LINE = "_____________________________________________________________________________________________";

    /**
     * 类冲突扫描器
     */
    private ClassConflictScanner classConflictScanner;

    /**
     * 配置构造器
     */
    private ConfigurationBuilder configurationBuilder;

    /**
     *只显示class文件不同的冲突
     */
    private Boolean onlyShowDiffClass = false;

    /**
     * 冲突统计数量
     */
    private int conflictCount = 0;

    private ConflictScanHelper() {
    }

    public static ConflictScanHelper getInstance() {
        ConflictScanHelper helper = new ConflictScanHelper();
        helper.classConflictScanner = new ClassConflictScanner();
        helper.configurationBuilder = new ConfigurationBuilder().setScanners(helper.classConflictScanner);
        return helper;
    }

    public ConflictScanHelper paths(String... paths) {
        Objects.requireNonNull(paths, "paths 路径不能为空");
        for (String path : paths) {
            this.configurationBuilder.addUrls(getUrlsFromScanPath(path));
        }
        return this;
    }

    public ConflictScanHelper urls(Collection<URL> urls) {
        Objects.requireNonNull(urls, "urls 不能为null");
        this.configurationBuilder.addUrls(urls);
        return this;
    }

    public ConflictScanHelper urls(URL... urls) {
        Objects.requireNonNull(urls, "urls 不能为null");
        this.configurationBuilder.addUrls(urls);
        return this;
    }

    /**
     * 文件过滤器
     * @param includeKeyword 包含（多个是或关系）
     * @param excludeKeyword 不包含（多个是与关系）
     */
    public ConflictScanHelper filter(String[] includeKeyword, String[] excludeKeyword) {
        if (!isEmpty(includeKeyword) && !isEmpty(excludeKeyword)) {
            this.configurationBuilder
                .setInputsFilter(input -> (isContain(input, includeKeyword) && !isContain(input, excludeKeyword)));
        } else if (!isEmpty(includeKeyword)) {
            this.configurationBuilder.setInputsFilter(input -> isContain(input, includeKeyword));
        } else if (!isEmpty(excludeKeyword)) {
            this.configurationBuilder.setInputsFilter(input -> !isContain(input, excludeKeyword));
        }
        return this;
    }

    public ConflictScanHelper forPackages(String... packages) {
        Objects.requireNonNull(packages, "packageName 不能为null");
        if (!isEmpty(packages) && this.classConflictScanner != null) {
            this.classConflictScanner.setFilterPackage(input -> isMatchPackages(input, packages));
        }
        return this;
    }

    public ConflictScanHelper onlyShowDiffClass(boolean onlyShowDiffClass) {
        this.onlyShowDiffClass = onlyShowDiffClass;
        return this;
    }

    /**
     * 获取冲突类以及路径信息
     */
    private Map<String, Set<String>> getConflicts() {
        return this.classConflictScanner.getConflictMap();
    }

    public void startScanConflict() {
        long start = System.currentTimeMillis();
        if (CollectionUtils.isEmpty(this.configurationBuilder.getUrls())) {
            //扫描默认路径
            this.paths(getCurrentPath());
        }
        //开始扫描文件
        new Reflections(this.configurationBuilder);
        //从classConflictScanner获取冲突的类
        Map<String, Set<String>> scanResultMap = getConflicts();
        //打印详细信息
        displayConflictClasses(scanResultMap);
        this.classConflictScanner.clear();
        long end = System.currentTimeMillis();
        LOG.error(SPLIT_LINE);
        LOG.error("共扫描到{}个冲突，耗时：{}ms", conflictCount, (end - start));
        LOG.error(SPLIT_LINE);
    }

    /**
     *打印冲突信息
     */
    private void displayConflictClasses(Map<String, Set<String>> conflicts) {
        HashSet<String> conflictJarSet = new HashSet<>(64);

        for (Entry<String, Set<String>> entry : conflicts.entrySet()) {
            Collection<String> conflictClassUrls = conflicts.get(entry.getKey());
            //比较冲突文件的MD5值是否一致
            HashSet<String> md5RepeatSet = new HashSet<>(conflictClassUrls.size());
            //md5校验结果
            Multimap<String, String> md5Values = ArrayListMultimap.create();

            for (String url : conflictClassUrls) {
                InputStream is = null;
                try {
                    String md5 = getMd5HexFromFile(url);
                    md5Values.put(md5, url);
                    md5RepeatSet.add(md5);
                } catch (Exception e) {
                    LOG.error("displayConflictClasses() 发生异常：{}", e);
                } finally {
                    CloseUtil.close(is);
                }
            }

            //展示冲突文件或者展示冲突并且md5不一致的class文件
            boolean display = !onlyShowDiffClass || md5RepeatSet.size() != 1;

            //打印具体哪几个类冲突了
            if (display) {
                conflictCount++;
                LOG.error(SPLIT_LINE);
                LOG.error("扫描到{}个同名的类：{}", conflictClassUrls.size(), entry.getKey());
                for (Map.Entry<String, String> e : md5Values.entries()) {
                    //url截取jar的路径
                    String jarPath = removeJarEntry(e.getValue());
                    if (StringUtils.isNotEmpty(jarPath)) {
                        conflictJarSet.add(jarPath);
                    }
                    LOG.error("文件(md5[{}])：{}", e.getKey(), e.getValue());
                }
            }
            //set里有多个MD5，表明文件不一致
            if (md5RepeatSet.size() > 1) {
                LOG.error("重名的类MD5值不一致，class文件不相同，请检查是否是jar包冲突");
            }
        }

        //打印冲突jar的路径
        LOG.error(SPLIT_LINE);
        if (conflictJarSet.size() > 0) {
            StringBuilder jars = new StringBuilder();
            for (String conflictJar : conflictJarSet) {
                jars.append(conflictJar).append("\n");
            }
            LOG.error("请检查以下jar包：\n{}", jars.toString());
        }
    }

    private Collection<URL> getUrlsFromScanPath(String path) {
        if (StringUtils.isBlank(path)) {
            LOG.error("getUrlsFromScanPath(): path不能是空字符串");
            return new HashSet<URL>();
        }
        //遍历文件结果
        Set<File> foundFiles = findFiles(path);
        Set<URL> urls = new HashSet<>(foundFiles.size());
        //文件转URL
        for (File file : foundFiles) {
            try {
                urls.add(file.toURI().toURL());
            } catch (MalformedURLException e) {
                LOG.error("getUrlsFromScanPath(): 创建URL失败，文件：{}，error:{}", file.getAbsoluteFile(), e);
            }
        }
        return urls;
    }

    /**
     * 非递归遍历指定目录里的jar和class文件
     */
    private Set<File> findFiles(String path) {
        return FileUtil.findFiles(path, this::isFound);
    }

    /**
     * 获取文件的MD5 HEx
     * @param url jar包中.class以[文件路径！类文件的相对jar包内部路径]来表示，
     * 非jar包中.class文件，就用绝对路径表示]
     */
    private String getMd5HexFromFile(String url) {
        String md5 = "";
        InputStream is = null;
        try {
            is = readSupportedFile(url);
            //获取文件的16进制md5
            md5 = DigestUtils.md5Hex(is);
        } catch (Exception e) {
            LOG.error("读取文件时发生异常：{}, error:{}", url, e);
        } finally {
            CloseUtil.close(is);
        }
        return md5;
    }

    /**
     * 读取jar内部文件和.class文件的输入流
     * @param url jar包中.class以[文件路径！类文件的相对jar包内部路径]来表示，
     * 非jar包中.class文件，就用绝对路径表示]
     */
    private InputStream readSupportedFile(String url) throws IOException {
        if (StringUtils.isBlank(url)) {
            throw new IllegalArgumentException("url 不能为空");
        }
        //".jar!"jar包路径标识
        String jarFlag = ".jar!";
        if (url.contains(jarFlag)) {
            String path = url.substring(0, url.lastIndexOf(jarFlag) + JAR_SUFFIX.length());
            String entry = url.substring(url.lastIndexOf(jarFlag) + jarFlag.length() + 1);
            return readJarFile(path, entry);
        } else if (url.endsWith(CLASS_SUFFIX)) {
            return readClassFile(url);
        }
        return null;
    }

    /**
     *根据.jar!标识判断是否是jar内部文件，如果是，返回jar路径
     */
    private String removeJarEntry(String url) {
        String path = null;
        if (StringUtils.isEmpty(url)) {
            return null;
        }
        //".jar!"jar包路径标识
        String jarFlag = ".jar!";
        if (url.contains(jarFlag)) {
            path = url.substring(0, url.lastIndexOf(jarFlag) + JAR_SUFFIX.length());
        } else if (url.endsWith(JAR_SUFFIX)) {
            path = url;
        }
        return path;
    }

    /**
     * 读取class文件输入流
     * @param path .class绝对路径
     */
    private InputStream readClassFile(String path) throws IOException {
        if (StringUtils.isNotBlank(path)) {
            if (!path.endsWith(CLASS_SUFFIX)) {
                throw new UnsupportedOperationException(path + "不是class文件");
            }
        }

        return new FileInputStream(new File(path));
    }

    /**
     * 从jar包中读取输入流
     * @param path ja包绝对路径
     * @param entry jar包内部文件,相对与jar路径
     */
    private InputStream readJarFile(String path, String entry) throws IOException {
        if (StringUtils.isNotBlank(path)) {
            if (!path.endsWith(JAR_SUFFIX)) {
                throw new UnsupportedOperationException(path + "不是jar文件");
            }
        }
        JarFile jarFile = new JarFile(path);
        ZipEntry ze = jarFile.getEntry(entry);
        return jarFile.getInputStream(ze);
    }

    /**
     *判定是否是classpath目录或jar包
     */
    private boolean isFound(File file) {
        return isClassesDirectory(file) || isJar(file);
    }

    private boolean isClass(File file) {
        if (exists(file) && file.isFile()) {
            return file.getName().endsWith(CLASS_SUFFIX);
        }
        return false;
    }

    /**
     *这里判定以classes结尾的目录是classpath路径
     */
    private boolean isClassesDirectory(File file) {
        if (exists(file) && file.isDirectory()) {
            return file.getName().endsWith(CLASSPATH_DIR_SUFFIX);
        }
        return false;
    }

    private boolean isJar(File file) {
        if (exists(file) && file.isFile()) {
            return file.getName().toLowerCase().endsWith(JAR_SUFFIX);
        }
        return false;
    }

    private String getCurrentPath() {
        String currentPath = "";
        try {
            currentPath = new File("").getAbsolutePath();
        } catch (Exception e) {
            LOG.error("获取当前路径失败：{}", e);
        }
        return currentPath;
    }

    /**
     *字符串是否含有子串
     * @param source 字符串
     * @param subStrings 子串
     */
    private boolean isContain(String source, String[] subStrings) {
        if (StringUtils.isEmpty(source)) {
            throw new UnsupportedOperationException("source字符串不能为空");
        }

        if (isEmpty(subStrings)) {
            throw new UnsupportedOperationException("subStrings[]字符串数组不能为空");
        }

        boolean contain = false;
        for (String sub : subStrings) {
            contain = (contain || source.contains(sub));
            if (contain) {
                return true;
            }
        }
        return false;
    }

    private boolean isEmpty(String... strings) {
        return strings == null || strings.length == 0;
    }

    /**
     * 包名过滤
     */
    private boolean isMatchPackages(String input, String... packages) {
        if (isEmpty(packages)) {
            throw new UnsupportedOperationException("input, packages不能为空");
        }

        if (StringUtils.isEmpty(input)) {
            return false;
        }

        boolean match = false;
        for (String s : packages) {
            match = match || input.startsWith(s);
            if (match) {
                return true;
            }
        }
        return false;
    }
}
